#include "OoO/bpu.h"
namespace config
{
    Switch ras_update_in_backend = {"ras_update_in_backend", "ras update in backend"};
} // namespace config
RAS::RAS(size_t ras_size) : ras(ras_size), ras_size(ras_size), ras_max_depth("ras-maxdepth")
{
    reset();
}
RAS::~RAS()
{
}
void RAS::reset()
{
    pointer = 0;
    ras_depth = 0;
}
RAS_Res RAS::pop()
{
    if (ras_depth > 0)
        ras_depth--;
    auto top = ras[pointer];
    auto top_pointer = pointer;
    if (!config::ras_update_in_backend)
    {
        pointer = (pointer - 1) % ras_size;
    }
    return {top, top_pointer};
}
RAS_Res RAS::push(RAS_Entry ras_entry)
{
    ras_depth++;
    if (ras_max_depth < ras_depth)
        ras_max_depth = ras_depth;
    auto top = ras[pointer];
    auto top_pointer = pointer;

    if (!config::ras_update_in_backend)
    {
        pointer = (pointer + 1) % ras_size;
        ras[pointer] = ras_entry;
    }

    return {top, top_pointer};
}

void RAS::update(word_t addr, BPU_Res correct_res)
{
    if (correct_res.ras_valid)
    {
        pointer = correct_res.ras_pointer;
        ras[pointer] = correct_res.ras_entry;
    }
    else if (config::ras_update_in_backend)
    {
        switch (correct_res.type)
        {
        case BranchType::Call:
            pointer = (pointer + 1) % ras_size;
            ras[pointer] = {addr + 4};
            break;
        case BranchType::Ret:
            pointer = (pointer - 1) % ras_size;
            break;
        case BranchType::CallRet:
            ras[pointer] = {addr + 4};
            break;
        default:
            break;
        }
    }
}

BPU::BPU(size_t btb_size, size_t ras_size) : ras(ras_size), btb(btb_size), btb_size(btb_size), ras_size(ras_size)
{
    reset();
}
BPU::~BPU()
{
}
void BPU::reset()
{
    for (auto &btb_entry : btb)
    {
        btb_entry.valid = false;
    }
}
BPU_Res BPU::predict(word_t addr)
{
    BPU_Res res = {0};
    word_t index = (addr / 4) % btb_size;
    auto &btb_entry = btb[index];
    bool btb_hit = btb_entry.valid && btb_entry.pc == addr;
    if (btb_hit)
    {
        res.valid = true;
        res.target = btb_entry.target;
        res.taken = true;
        res.type = btb_entry.type;
        RAS_Res ras_res = {0};
        switch (btb_entry.type)
        {
        case BranchType::Branch:
            res.btb_sc = btb_entry.sc;
            if (btb_entry.sc < 2)
            {
                res.taken = false;
                if (btb_entry.sc > 0)
                    btb_entry.sc--;
            }
            else
            {
                if (btb_entry.sc < 3)
                    btb_entry.sc++;
            }
            break;
        case BranchType::Jump:
            break;
        case BranchType::Call:
            ras_res = ras.push({addr + 4});
            res.ras_valid = true;
            break;
        case BranchType::Ret:
            ras_res = ras.pop();
            res.target = ras_res.entry.target;
            res.ras_valid = true;
            break;
        case BranchType::CallRet:
            ras_res = ras.pop();
            res.target = ras_res.entry.target;
            res.ras_valid = true;
            ras.push({addr + 4});
            break;
        default:
            break;
        }
        res.ras_pointer = ras_res.pointer;
        res.ras_entry = ras_res.entry;
    }
    return res;
}
void BPU::update(word_t addr, BPU_Res correct_res)
{
    SPDLOG_TRACE("[BPU] update branch pc {:08x} target {:08x} type {}", addr, correct_res.target, correct_res.type);
    word_t index = (addr / 4) % btb_size;
    auto &btb_entry = btb[index];
    btb_entry.valid = true;
    btb_entry.pc = addr;
    btb_entry.target = correct_res.target;
    btb_entry.type = correct_res.type;

    /* 2 bit counter update */
    if (correct_res.type == BranchType::Branch)
    {
        uint32_t sc = correct_res.btb_sc;
        if (correct_res.taken && sc < 3)
        {
            sc++;
        }
        else if (!correct_res.taken && sc > 0)
        {
            sc--;
        }
        btb_entry.sc = sc;
    }
    else
    {
        btb_entry.sc = 0;
    }

    /* RAS update */
    if (correct_res.type == BranchType::Call || correct_res.type == BranchType::CallRet || correct_res.type == BranchType::Ret)
    {
        ras.update(addr, correct_res);
    }
}